package org.thanos.bacnet.sero.warp;

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalUnit;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

public class WarpClock extends Clock {
    private final ZoneId zoneId;
    private LocalDateTime dateTime;
    private final List<ClockListener> listeners;

    public WarpClock() {
        this(ZoneId.systemDefault());
    }

    public WarpClock(ZoneId zoneId) {
        this(zoneId, LocalDateTime.now(Clock.system(zoneId)));
    }

    public WarpClock(ZoneId zoneId, LocalDateTime dateTime) {
        this.listeners = new CopyOnWriteArrayList<>();
        Objects.requireNonNull(zoneId, "zoneId");
        Objects.requireNonNull(dateTime, "dateTime");
        this.zoneId = zoneId;
        this.dateTime = dateTime;
    }

    public TimeoutFuture<?> setTimeout(Runnable command, long timeout, TimeUnit timeUnit) {
        return this.setTimeout(() -> {
            command.run();
            return null;
        }, timeout, timeUnit);
    }

    public <V> TimeoutFuture<V> setTimeout(final Callable<V> callable, long timeout, TimeUnit timeUnit) {
        final LocalDateTime deadline = this.dateTime.plusNanos(timeUnit.toNanos(timeout));
        final WarpClock.TimeoutFutureImpl<V> future = new TimeoutFutureImpl<>();
        ClockListener listener = new ClockListener() {
            public void clockUpdate(LocalDateTime dateTime) {
                if (!dateTime.isBefore(deadline)) {
                    if (!future.isCancelled()) {
                        try {
                            future.setResult(callable.call());
                        } catch (Exception var3) {
                            future.setException(var3);
                        }
                    }

                    WarpClock.this.listeners.remove(this);
                }

            }
        };
        this.listeners.add(listener);
        return future;
    }

    public void addListener(ClockListener listener) {
        this.listeners.add(listener);
    }

    public void removeListener(ClockListener listener) {
        this.listeners.remove(listener);
    }

    public LocalDateTime set(int year, Month month, int dayOfMonth, int hour, int minute) {
        return this.fireUpdate(LocalDateTime.of(year, month, dayOfMonth, hour, minute));
    }

    public LocalDateTime set(int year, Month month, int dayOfMonth, int hour, int minute, int second) {
        return this.fireUpdate(LocalDateTime.of(year, month, dayOfMonth, hour, minute, second));
    }

    public LocalDateTime set(int year, Month month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond) {
        return this.fireUpdate(LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond));
    }

    public LocalDateTime set(int year, int month, int dayOfMonth, int hour, int minute) {
        return this.fireUpdate(LocalDateTime.of(year, month, dayOfMonth, hour, minute));
    }

    public LocalDateTime set(int year, int month, int dayOfMonth, int hour, int minute, int second) {
        return this.fireUpdate(LocalDateTime.of(year, month, dayOfMonth, hour, minute, second));
    }

    public LocalDateTime set(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond) {
        return this.fireUpdate(LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond));
    }

    public LocalDateTime plus(TemporalAmount amountToAdd) {
        return this.fireUpdate(this.dateTime.plus(amountToAdd));
    }

    public LocalDateTime plus(long amountToAdd, TemporalUnit unit) {
        return this.fireUpdate(this.dateTime.plus(amountToAdd, unit));
    }

    public LocalDateTime plusYears(long years) {
        return this.fireUpdate(this.dateTime.plusYears(years));
    }

    public LocalDateTime plusMonths(long months) {
        return this.fireUpdate(this.dateTime.plusMonths(months));
    }

    public LocalDateTime plusWeeks(long weeks) {
        return this.fireUpdate(this.dateTime.plusWeeks(weeks));
    }

    public LocalDateTime plusDays(long days) {
        return this.fireUpdate(this.dateTime.plusDays(days));
    }

    public LocalDateTime plusHours(long hours) {
        return this.fireUpdate(this.dateTime.plusHours(hours));
    }

    public LocalDateTime plusMinutes(long minutes) {
        return this.fireUpdate(this.dateTime.plusMinutes(minutes));
    }

    public LocalDateTime plusSeconds(long seconds) {
        return this.fireUpdate(this.dateTime.plusSeconds(seconds));
    }

    public LocalDateTime plusMillis(long millis) {
        return this.fireUpdate(this.dateTime.plusNanos(millis * 1000000L));
    }

    public LocalDateTime plusNanos(long nanos) {
        return this.fireUpdate(this.dateTime.plusNanos(nanos));
    }

    public LocalDateTime plus(int amount, TimeUnit unit, long endSleep) {
        return this.plus(amount, unit, 0, (TimeUnit) null, 0L, endSleep);
    }

    public LocalDateTime plus(int amount, TimeUnit unit, int byAmount, TimeUnit byUnit, long eachSleep, long endSleep) {
        long remainder = unit.toNanos(amount);
        long each = (byUnit == null ? unit : byUnit).toNanos(byAmount == 0 ? (long) amount : (long) byAmount);
        LocalDateTime result = null;

        try {
            if (remainder <= 0L) {
                result = this.plusNanos(0L);
                Thread.sleep(eachSleep);
            } else {
                while (remainder > 0L) {
                    long nanos = each;
                    if (each > remainder) {
                        nanos = remainder;
                    }

                    result = this.plusNanos(nanos);
                    remainder -= nanos;
                    Thread.sleep(eachSleep);
                }
            }

            Thread.sleep(endSleep);
            return result;
        } catch (InterruptedException var16) {
            throw new RuntimeException(var16);
        }
    }

    private LocalDateTime fireUpdate(LocalDateTime newDateTime) {
        this.dateTime = newDateTime;

        for (ClockListener l : this.listeners) {
            l.clockUpdate(newDateTime);
        }

        return this.dateTime;
    }

    public int get(TemporalField field) {
        return this.dateTime.get(field);
    }

    public long getLong(TemporalField field) {
        return this.dateTime.getLong(field);
    }

    public LocalDateTime getDateTime() {
        return this.dateTime;
    }

    public ZoneId getZone() {
        return this.zoneId;
    }

    public Clock withZone(ZoneId zone) {
        return new WarpClock(this.zoneId, this.dateTime);
    }

    public Instant instant() {
        return this.dateTime.atZone(this.zoneId).toInstant();
    }

    static class TimeoutFutureImpl<V> implements TimeoutFuture<V> {
        private boolean success;
        private boolean cancelled;
        private Exception ex;
        private V result;
        private volatile boolean done;

        TimeoutFutureImpl() {
        }

        public V get() throws CancellationException, InterruptedException, Exception {
            if (this.success) {
                return this.result;
            } else if (this.ex != null) {
                throw this.ex;
            } else if (this.cancelled) {
                throw new CancellationException();
            } else {
                synchronized (this) {
                    this.wait();
                }

                if (this.success) {
                    return this.result;
                } else if (this.ex != null) {
                    throw this.ex;
                } else {
                    throw new CancellationException();
                }
            }
        }

        public synchronized boolean isCancelled() {
            return this.cancelled;
        }

        public synchronized boolean cancel() {
            if (!this.done) {
                this.cancelled = true;
                this.done();
                return true;
            } else {
                return false;
            }
        }

        synchronized void setResult(V result) {
            this.result = result;
            this.success = true;
            this.done();
        }

        synchronized void setException(Exception ex) {
            this.ex = ex;
            this.done();
        }

        void done() {
            this.notifyAll();
            this.done = true;
        }
    }
}

